Day 26 在追 Closure 時,有一件事情其實很好理解:
function outer() {
const name = 'Leo'
return function inner() {
console.log(name)
}
}
inner 要找 name,可以往它建立時的外層 Scope 找。
所以我很自然地開始想:
那 this 呢?
是不是也可以看函式「寫在哪裡」,就知道 this 是誰?
結果不是。
而且我以前一直把 this 理解成:
this就是「這個物件」。
這個說法有時候看起來會對,然後在下一段程式碼突然把人騙死 XD
今天只追一件事:
一般 function 裡的 this,到底是怎麼決定的?
const user = {
name: 'Leo',
sayName() {
console.log(this.name)
}
}
user.sayName()
結果:
Leo
看起來很好懂。
sayName 在 user 裡面。
所以:
this === user
然後:
this.name
就是:
user.name
如果只看到這裡,我大概會得到一個結論:
function 寫在誰裡面,
this就是誰。
但現在做一個很小的修改。
const user = {
name: 'Leo',
sayName() {
console.log(this.name)
}
}
const speak = user.sayName
speak()
speak 裡面裝的是什麼?
就是原本那個 function。
我們沒有重新寫一個。
也沒有修改它。
console.log(speak === user.sayName)
結果:
true
明明是同一個 function。
那:
speak()
還會印出 Leo 嗎?
在現在常見的 ES Module/strict mode 環境裡,這裡的 this 會是 undefined。
所以執行:
this.name
甚至可能直接得到錯誤:
TypeError
等一下。
function 明明一模一樣。
到底是哪裡變了?
比較這兩行:
user.sayName()
和:
speak()
function 本身沒有改。
改的是 call site,也就是呼叫方式。
第一種:
user.sayName()
可以先很粗略地讀成:
user
↓
呼叫 sayName()
這次呼叫裡:
this === user
但第二種:
speak()
前面已經沒有:
user.
了。
它現在是一個普通的 function call。
所以不能因為這個 function「以前住在 user 裡」,就認定 this 永遠是 user。
這跟 Closure 的思路真的不一樣。
Day 24 到 Day 26 一直在處理這件事:
const name = 'outside'
function test() {
console.log(name)
}
name 怎麼找?
這會跟 Lexical Scope 有關。
也就是 function 寫在哪裡、建立在哪個詞法環境裡。
可以粗略想成:
function 建立的位置
↓
Scope Chain
↓
找外層變數
可是一般 function 的 this,不能用同一套規則直接推。
它更在意:
這一次 function 是怎麼被呼叫的?
```
這是我覺得最容易把 `this` 跟 Closure 混在一起的地方。
兩個東西都出現在 function 裡。
但判斷方法完全不同。
## 同一個 function 可以有不同的 this
再做一個更明顯的實驗。
~~~js
function introduce() {
console.log(`我是 ${this.name}`)
}
const userA = {
name: '小明',
introduce
}
const userB = {
name: '小華',
introduce
}
注意:
userA.introduce === userB.introduce
結果是:
true
它們用的是同一個 function。
現在:
userA.introduce()
得到:
我是 小明
然後:
userB.introduce()
得到:
我是 小華
所以如果問:
introduce的this是誰?
其實這個問題少了一個很重要的條件。
比較精確的問法應該是:
這一次呼叫
introduce()時,this是誰?
第一次:
userA.introduce()
所以這次是:
this === userA
第二次:
userB.introduce()
所以這次變成:
this === userB
同一段 function 程式碼,可以得到不同的 this。
這件事情如果沒先弄懂,後面看到 callback 很容易開始懷疑人生。
假設有一個物件:
const user = {
name: 'Leo',
sayName() {
console.log(this.name)
}
}
直接呼叫:
user.sayName()
沒問題。
但如果把它當成 callback 傳出去:
function run(callback) {
callback()
}
run(user.sayName)
注意 run 收到的是:
user.sayName
也就是 function 這個值。
到了 run 裡面真正執行時:
callback()
呼叫方式已經不是:
user.sayName()
而是:
callback()
這時原本的 user. 已經不在 call site 上了。
所以「把 method 傳出去」這件事,不會自動把原本的 this 一起永久黏著帶走。
這裡跟 Closure 又形成很有趣的對比。
Closure 可以讓 function 持續存取它建立時的外層變數。
但:
const callback = user.sayName
不代表 callback 永遠保有:
this === user
然後 JavaScript 又跑出另一種 function:
const sayName = () => {
console.log(this)
}
Arrow Function 的 this 又不走剛才那套。
它沒有自己的 this binding。
它會使用外層詞法環境中的 this。
這也是為什麼下面這種寫法很容易跟想像不同:
const user = {
name: 'Leo',
sayName: () => {
console.log(this.name)
}
}
user.sayName()
很多人第一眼可能會想:
this === user
但 Arrow Function 不會因為你寫成:
user.sayName()
就得到自己的 this = user。
它的 this 是從外層取得的。
所以這種情況不能拿一般 method 的規則直接套。
這也讓我終於比較能理解,為什麼 JavaScript 同時存在:
function () {}
和:
() => {}
它們並不只是「一個比較舊、一個比較短」。
行為真的有差。
以前看到:
this.name
我會下意識把它翻譯成:
這個物件的 name。
現在我會先停一下。
如果是一般 function,我先找:
這個 function 是怎麼被呼叫的?
```
例如:
~~~js
user.sayName()
跟:
const fn = user.sayName
fn()
即使 fn 和 user.sayName 是同一個 function:
fn === user.sayName
也不代表兩次呼叫的 this 一樣。
所以 this 不是物件的永久身分證。
它比較像是這一次 function 執行時得到的一個呼叫背景。
現在終於可以把 Day 26 和 Day 27 接起來。
const name = 'outside'
const user = {
name: 'Leo',
sayName() {
console.log(name)
console.log(this.name)
}
}
裡面有兩個 name。
第一個:
name
會走 Scope Chain。
看的是:
sayName建立在哪個 Lexical Environment?
第二個:
this.name
一般 function 則要先問:
這一次
sayName是怎麼被呼叫的?
所以:
name
↓
Lexical Scope
↓
函式寫在哪裡
this
↓
Call Site
↓
函式怎麼被呼叫
這兩條線終於分開了。
function showName() {
console.log(this.name)
}
const a = {
name: 'A',
showName
}
const b = {
name: 'B',
showName
}
const fn = a.showName
a.showName()
b.showName()
fn()
先不要跑。
前兩個應該不難。
第三個才是今天真正要看的地方。
前兩次:
A
B
因為呼叫方式分別是:
a.showName()
b.showName()
第三次:
fn()
已經失去 a. 這個呼叫形式。
在 ES Module/strict mode 環境下,this 是 undefined,所以存取:
this.name
會拋出 TypeError。
一般 function 的
this不能只看它寫在哪裡,而要看這一次它是怎麼被呼叫的。
Scope Chain 比較像在問:
我出生在哪裡?
this 比較像在問:
這次是誰用什麼方式叫我?
這也解釋了為什麼同一個 function,可以在不同呼叫中得到不同的 this。
而 Arrow Function 又是另一回事:它沒有自己的 this,而是沿用外層的 this。
今天第一次看到一件很重要的事情:
run(user.sayName)
我們可以把一個 function 交給另一個 function,然後由它「之後再呼叫」。
可是這又冒出另一個問題。
如果寫:
setTimeout(() => {
console.log('hello')
}, 1000)
JavaScript 明明一次只能執行一件事情。
那這個 function 到底去了哪裡?
一秒到了,是誰把它叫回來?
如果 JavaScript 是單執行緒,setTimeout 為什麼不會卡在那邊等一秒?
下一篇開始往非同步走。